/*-------------------------------------------------------------------------
 *
 * backup_manifest.c
 *	  生成和发送备份清单的代码
 *
 * Portions Copyright (c) 2010-2022, PostgreSQL Global Development Group
 *
 * IDENTIFICATION
 *	  src/backend/backup/backup_manifest.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/timeline.h"
#include "backup/backup_manifest.h"
#include "backup/basebackup_sink.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "mb/pg_wchar.h"
#include "utils/builtins.h"
#include "utils/json.h"

static void fc_AppendStringToManifest(backup_manifest_info *fc_manifest, char *fc_s);

/*
 * 用户想要备份清单吗？
 *
 * 始终拥有一个 manifest_info 对象是最简单的，这样我们就不需要在太多地方检查 NULL 指针。然而，如果用户不想要清单，我们将 manifest->buffile 设置为 NULL。
 */
static inline bool fc_IsManifestEnabled(backup_manifest_info *fc_manifest)
{
	return (fc_manifest->buffile != NULL);
}

/*
 * 便捷宏用于将数据附加到备份清单中。
 */
#define AppendToManifest(manifest, ...) \
	{ \
		char *fc__manifest_s = psprintf(__VA_ARGS__);	\
		fc_AppendStringToManifest(manifest, fc__manifest_s);	\
		pfree(fc__manifest_s);	\
	}

/*
 * 初始化状态，以便我们可以构建备份清单。
 *
 * 注意：尽管数据文件的校验和类型是可配置的，但清单本身的校验和总是使用 SHA-256。详见 SendBackupManifest 中的注释。
 */
void InitializeBackupManifest(backup_manifest_info *fc_manifest,
						 backup_manifest_option fc_want_manifest,
						 pg_checksum_type fc_manifest_checksum_type)
{
	memset(fc_manifest, 0, sizeof(backup_manifest_info));
	fc_manifest->checksum_type = fc_manifest_checksum_type;

	if (fc_want_manifest == MANIFEST_OPTION_NO)
		fc_manifest->buffile = NULL;
	else
	{
		fc_manifest->buffile = BufFileCreateTemp(false);
		fc_manifest->manifest_ctx = pg_cryptohash_create(PG_SHA256);
		if (pg_cryptohash_init(fc_manifest->manifest_ctx) < 0)
			elog(ERROR, "failed to initialize checksum of backup manifest: %s",
				 pg_cryptohash_error(fc_manifest->manifest_ctx));
	}

	fc_manifest->manifest_size = UINT64CONST(0);
	fc_manifest->force_encode = (fc_want_manifest == MANIFEST_OPTION_FORCE_ENCODE);
	fc_manifest->first_file = true;
	fc_manifest->still_checksumming = true;

	if (fc_want_manifest != MANIFEST_OPTION_NO)
		AppendToManifest(fc_manifest,
						 "{ \"PostgreSQL-Backup-Manifest-Version\": 1,\n"
						 "\"Files\": [");
}

/*
 * 释放分配给构建的备份清单的资源。
 */
void FreeBackupManifest(backup_manifest_info *fc_manifest)
{
	pg_cryptohash_free(fc_manifest->manifest_ctx);
	fc_manifest->manifest_ctx = NULL;
}

/*
 * 为文件添加一条备份清单条目。
 */
void AddFileToBackupManifest(backup_manifest_info *fc_manifest, const char *fc_spcoid,
						const char *fc_pathname, size_t fc_size, pg_time_t fc_mtime,
						pg_checksum_context *fc_checksum_ctx)
{
	char		fc_pathbuf[MAXPGPATH];
	int			fc_pathlen;
	StringInfoData fc_buf;

	if (!fc_IsManifestEnabled(fc_manifest))
		return;

	/*
	 * 如果此文件是某个表空间的一部分，传递给此函数的路径名将相对于包含它的 tar 文件。我们希望路径名相对于数据目录（忽略中间的符号链接遍历）。
	 */
	if (fc_spcoid != NULL)
	{
		snprintf(fc_pathbuf, sizeof(fc_pathbuf), "pg_tblspc/%s/%s", fc_spcoid,
				 fc_pathname);
		fc_pathname = fc_pathbuf;
	}

	/*
	 * 每个文件的条目需要用逗号与后面的条目分开，但第一个条目之前和最后一个条目之后没有逗号。为了使这有效，向清单添加文件从终止最近添加的行开始，如果合适则加上逗号，但不终止为此文件插入的行。
	 */
	initStringInfo(&fc_buf);
	if (fc_manifest->first_file)
	{
		appendStringInfoChar(&fc_buf, '\n');
		fc_manifest->first_file = false;
	}
	else
		appendStringInfoString(&fc_buf, ",\n");

	/*
	 * 将此文件的相对路径名写入清单中。清单总是以 UTF-8 存储，因此我们必须对不符合该编码的路径进行编码。
	 */
	fc_pathlen = strlen(fc_pathname);
	if (!fc_manifest->force_encode &&
		pg_verify_mbstr(PG_UTF8, fc_pathname, fc_pathlen, true))
	{
		appendStringInfoString(&fc_buf, "{ \"Path\": ");
		escape_json(&fc_buf, fc_pathname);
		appendStringInfoString(&fc_buf, ", ");
	}
	else
	{
		appendStringInfoString(&fc_buf, "{ \"Encoded-Path\": \"");
		enlargeStringInfo(&fc_buf, 2 * fc_pathlen);
		fc_buf.len += hex_encode(fc_pathname, fc_pathlen,
							  &fc_buf.data[fc_buf.len]);
		appendStringInfoString(&fc_buf, "\", ");
	}

	appendStringInfo(&fc_buf, "\"Size\": %zu, ", fc_size);

	/*
	 * 将最后修改时间转换为字符串并附加到清单中。由于不清楚使用哪个时区，并且时区定义可能会更改，可能造成混淆，因此始终使用 GMT。
	 */
	appendStringInfoString(&fc_buf, "\"Last-Modified\": \"");
	enlargeStringInfo(&fc_buf, 128);
	fc_buf.len += pg_strftime(&fc_buf.data[fc_buf.len], 128, "%Y-%m-%d %H:%M:%S %Z",
						   pg_gmtime(&fc_mtime));
	appendStringInfoChar(&fc_buf, '"');

	/* 添加校验和信息。 */
	if (fc_checksum_ctx->type != CHECKSUM_TYPE_NONE)
	{
		uint8		fc_checksumbuf[PG_CHECKSUM_MAX_LENGTH];
		int			fc_checksumlen;

		fc_checksumlen = pg_checksum_final(fc_checksum_ctx, fc_checksumbuf);
		if (fc_checksumlen < 0)
			elog(ERROR, "could not finalize checksum of file \"%s\"",
				 fc_pathname);

		appendStringInfo(&fc_buf,
						 ", \"Checksum-Algorithm\": \"%s\", \"Checksum\": \"",
						 pg_checksum_type_name(fc_checksum_ctx->type));
		enlargeStringInfo(&fc_buf, 2 * fc_checksumlen);
		fc_buf.len += hex_encode((char *) fc_checksumbuf, fc_checksumlen,
							  &fc_buf.data[fc_buf.len]);
		appendStringInfoChar(&fc_buf, '"');
	}

	/* 关闭对象。 */
	appendStringInfoString(&fc_buf, " }");

	/* 好的，将其添加到清单中。 */
	fc_AppendStringToManifest(fc_manifest, fc_buf.data);

	/* 避免内存泄漏。 */
	pfree(fc_buf.data);
}

/*
 * 添加关于在恢复此备份时需要重放的 WAL 信息到清单中。
 */
void AddWALInfoToBackupManifest(backup_manifest_info *fc_manifest, XLogRecPtr fc_startptr,
						   TimeLineID fc_starttli, XLogRecPtr fc_endptr,
						   TimeLineID fc_endtli)
{
	List	   *fc_timelines;
	ListCell   *fc_lc;
	bool		fc_first_wal_range = true;
	bool		fc_found_start_timeline = false;

	if (!fc_IsManifestEnabled(fc_manifest))
		return;

	/* 终止文件列表。 */
	fc_AppendStringToManifest(fc_manifest, "\n],\n");

	/* 读取结束时间线的时间线历史。 */
	fc_timelines = readTimeLineHistory(fc_endtli);

	/* 开始一系列 LSN 范围。 */
	fc_AppendStringToManifest(fc_manifest, "\"WAL-Ranges\": [\n");

	foreach(fc_lc, fc_timelines)
	{
		TimeLineHistoryEntry *fc_entry = lfirst(fc_lc);
		XLogRecPtr	fc_tl_beginptr;

		/*
		 * 我们只关心在备份期间活动的时间线。跳过任何在备份开始之前结束的时间线。（注意，如果 entry->end 是 InvalidXLogRecPtr，则意味着该时间线尚未结束。）
		 */
		if (!XLogRecPtrIsInvalid(fc_entry->end) && fc_entry->end < fc_startptr)
			continue;

		/*
		 * 由于时间线历史文件将较新的时间线列在较旧的时间线之前，因此我们遇到的第一个新到足以重要的时间线应该与备份的结束时间线匹配。
		 */
		if (fc_first_wal_range && fc_endtli != fc_entry->tli)
			ereport(ERROR,
					errmsg("expected end timeline %u but found timeline %u",
						   fc_starttli, fc_entry->tli));

		/*
		 * 如果此时间线条目与备份开始时的时间线匹配，则需要从备份的起始 LSN 检查 WAL。如果此条目指向一个较新的时间线，则需要从该时间线的起始 LSN 检查 WAL，因此使用时间线开始时的 LSN。
		 */
		if (fc_starttli == fc_entry->tli)
			fc_tl_beginptr = fc_startptr;
		else
		{
			fc_tl_beginptr = fc_entry->begin;

			/*
			 * 如果我们到达一个没有有效起始LSN的TLI，那么在此点之后的历史中就不可能有更多的时间线，
			 * 所以我们最好已经到达预期的起始TLI。如果没有，那么就出大问题了。
			 */
			if (XLogRecPtrIsInvalid(fc_entry->begin))
				ereport(ERROR,
						errmsg("expected start timeline %u but found timeline %u",
							   fc_starttli, fc_entry->tli));
		}

		AppendToManifest(fc_manifest,
						 "%s{ \"Timeline\": %u, \"Start-LSN\": \"%X/%X\", \"End-LSN\": \"%X/%X\" }",
						 fc_first_wal_range ? "" : ",\n",
						 fc_entry->tli,
						 LSN_FORMAT_ARGS(fc_tl_beginptr),
						 LSN_FORMAT_ARGS(fc_endptr));

		if (fc_starttli == fc_entry->tli)
		{
			fc_found_start_timeline = true;
			break;
		}

		fc_endptr = fc_entry->begin;
		fc_first_wal_range = false;
	}

	/*
	 * 结束时间线的时间线历史中的最后一项应该是结束时间线本身。验证我们观察到的是否是这样。
	 */
	if (!fc_found_start_timeline)
		ereport(ERROR,
				errmsg("start timeline %u not found in history of timeline %u",
					   fc_starttli, fc_endtli));

	/* 终止WAL范围列表。 */
	fc_AppendStringToManifest(fc_manifest, "\n],\n");
}

/*
 * 完成备份清单，并将其发送给客户端。
 */
void SendBackupManifest(backup_manifest_info *fc_manifest, bbsink *fc_sink)
{
	uint8		fc_checksumbuf[PG_SHA256_DIGEST_LENGTH];
	char		fc_checksumstringbuf[PG_SHA256_DIGEST_STRING_LENGTH];
	size_t		fc_manifest_bytes_done = 0;

	if (!fc_IsManifestEnabled(fc_manifest))
		return;

	/*
	 * 附加清单校验和，以便可以检测到清单本身的任何问题。
	 *
	 * 我们始终使用SHA-256来实现这一点，无论选择何种算法对文件进行校验和计算。如果我们以后想要使清单文件使用的校验和算法可变，
	 * 客户端需要一种方法来确定使用哪种算法，尽可能接近清单文件的开头，以避免必须读取整个文件两次。
	 */
	fc_manifest->still_checksumming = false;
	if (pg_cryptohash_final(fc_manifest->manifest_ctx, fc_checksumbuf,
							sizeof(fc_checksumbuf)) < 0)
		elog(ERROR, "failed to finalize checksum of backup manifest: %s",
			 pg_cryptohash_error(fc_manifest->manifest_ctx));
	fc_AppendStringToManifest(fc_manifest, "\"Manifest-Checksum\": \"");

	hex_encode((char *) fc_checksumbuf, sizeof fc_checksumbuf, fc_checksumstringbuf);
	fc_checksumstringbuf[PG_SHA256_DIGEST_STRING_LENGTH - 1] = '\0';

	fc_AppendStringToManifest(fc_manifest, fc_checksumstringbuf);
	fc_AppendStringToManifest(fc_manifest, "\"}\n");

	/*
	 * 我们已经将所有数据写入清单文件。将文件倒带，以便我们可以读取回来。
	 */
	if (BufFileSeek(fc_manifest->buffile, 0, 0L, SEEK_SET))
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not rewind temporary file")));


	/*
	 * 发送备份清单。
	 */
	bbsink_begin_manifest(fc_sink);
	while (fc_manifest_bytes_done < fc_manifest->manifest_size)
	{
		size_t		fc_bytes_to_read;
		size_t		fc_rc;

		fc_bytes_to_read = Min(fc_sink->bbs_buffer_length,
							fc_manifest->manifest_size - fc_manifest_bytes_done);
		fc_rc = BufFileRead(fc_manifest->buffile, fc_sink->bbs_buffer,
						 fc_bytes_to_read);
		if (fc_rc != fc_bytes_to_read)
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not read from temporary file: read only %zu of %zu bytes",
							fc_rc, fc_bytes_to_read)));
		bbsink_manifest_contents(fc_sink, fc_bytes_to_read);
		fc_manifest_bytes_done += fc_bytes_to_read;
	}
	bbsink_end_manifest(fc_sink);

	/* 释放资源 */
	BufFileClose(fc_manifest->buffile);
}

/*
 * 向清单附加一个cstring。
 */
static void fc_AppendStringToManifest(backup_manifest_info *fc_manifest, char *fc_s)
{
	int			fc_len = strlen(fc_s);

	Assert(fc_manifest != NULL);
	if (fc_manifest->still_checksumming)
	{
		if (pg_cryptohash_update(fc_manifest->manifest_ctx, (uint8 *) fc_s, fc_len) < 0)
			elog(ERROR, "failed to update checksum of backup manifest: %s",
				 pg_cryptohash_error(fc_manifest->manifest_ctx));
	}
	BufFileWrite(fc_manifest->buffile, fc_s, fc_len);
	fc_manifest->manifest_size += fc_len;
}
